<?php
namespace Wcs\Upload;

use Wcs;
use Wcs\Http\PutPolicy;
use Wcs\Config;

class ResumeUploader
{

    //需要传入的参数
    private $blockSize;
    private $chunkSize;
    private $countForRetry;
    private $timeoutForRetry;

    //用户自定义信息
    private $userParam;
    private $encodedUserVars;
    private $mimeType;

    //断点续传信息
    private $localFile;
    private $blockNumOfUploaded;
    private $chunkNumOfUploaded;
    private $ctxListForMkfile;
    private $sizeOfFile;
    private $sizeOfUploaded;
    private $latestChunkCtx;
    private $time;



    //uuid随机数用 php 的uniqid()
    private $uuid;

    //断点续传
    private $recordFile;


    public function __construct(
        $userParam,
        $encodedUserVars,
        $mimeType
    ) {

        $this->blockSize = Config::WCS_BLOCK_SIZE;
        $this->chunkSize = Config::WCS_CHUNK_SIZE;
        $this->countForRetry = Config::WCS_COUNT_FOR_RETRY;
        $this->timeoutForRetry = Config::WCS_TIMEOUT_FOR_RETRY;
        $this->blockNumOfUploaded = 0;
        $this->chunkNumOfUploaded = 0;
        $this->ctxListForMkfile = array();
        $this->ctxList = array();
        $this->sizeOfFile = 0;
        $this->sizeOfUploaded = 0;
        $this->latestChunkCtx = '';
        $this->uuid = '';
        $this->time = 0;
        $this->recordFile = '';

        //默认用户自定义信息
        $this->userParam = $userParam;
        $this->encodedUserVars = $encodedUserVars;
        $this->mimeType = $mimeType;
    }


    /**
     * 分片上传
     * @param $bucketName
     * @param $fileName
     * @param $localFile
     * @param $returnBody
     * @return string
     */

    function upload($bucketName, $fileName, $localFile) {
        $pp = new PutPolicy();

        //默认不覆盖
        $pp->overwrite = Config::WCS_OVERWRITE;

        $resp = $this->_upload($bucketName, $fileName, $localFile, $pp);
        $respBody = \Wcs\url_safe_base64_decode($resp->respBody);

        return $respBody;
    }

    function _upload($bucketName, $fileName, $localFile, $putPolicy) {

        if ($fileName == null || $fileName === '') {
            $putPolicy->scope = $bucketName;
        } else {
            $putPolicy->scope = $bucketName . ':' . $fileName;
        }

        $this->localFile = $localFile;

        //记录文件后缀.rcd, WCS_RECORD_URL 为记录文件的路径，默认是上传文件当前路径
        $this->recordFile = Config::WCS_RECORD_URL.$localFile.'.rcd';

        $this->sizeOfFile= filesize($localFile);

        //如果有断点续传记录，从记录文件中读取信息
        if(file_exists($this->recordFile)) {

            //获取记录文件最后一行的记录
            $info = exec("tail -1 {$this->recordFile}");

            $info = json_decode($info, true);
            $token = $info['token'];
            $this->blockNumOfUploaded = $info['blockNumOfUploaded'];
            $this->chunkNumOfUploaded = $info['chunkNumOfUploaded'];
            $this->sizeOfUploaded = $info['sizeOfUploaded'];
            $this->ctxListForMkfile = $info['ctxListForMkfile'];
            $this->latestChunkCtx = $info['latestChunkCtx'];
            $this->uuid = $info['uuid'];

            //判断token是否过期
            $now = time();
            if(($now - $info['time']) > 3600) {
                $this->time = time();
                $token = $putPolicy->get_token(null);
            }
            else {
                $this->time = $info['time'];
            }

        }else {
            //获取token
            $this->uuid = uniqid();
            $this->time = time();
            $token = $putPolicy->get_token(null);
        }


        $resp = $this->resumeUpload($token);

        return $resp;

    }

    function resumeUpload($token){

        //打开文件，指定从断点地方开始读取
        $handle = fopen($this->localFile, "r") or die("ERROR:{$this->localFile}文件打开失败！");
        $recordFile = fopen($this->recordFile, "a") or die("ERROR:{$this->recordFile}文件打开失败！");

        //定位到文件上次中断的位置
        $offset = 
            ($this->blockNumOfUploaded) * ($this->blockSize) + 
            ($this->chunkNumOfUploaded) * ($this->chunkSize) ;

        if(fseek($handle, $offset) == -1) {
            die("ERROR:读取文件出错！");
        }

        //片大小必须是块大小的整数倍
        $blockNum = ceil($this->sizeOfFile / ($this->blockSize));


        //分片上传逻辑，先分块再分片顺序上传
        for($curBlockNum = $this->blockNumOfUploaded; $curBlockNum < $blockNum; $curBlockNum ++) {

            if($this->sizeOfUploaded < $this->sizeOfFile) {

                //判断是否是最后一块，如果是最后一块，按照块的实际大小
                if($curBlockNum == $blockNum - 1) {
                    $curBlockSize = $this->sizeOfFile - $this->sizeOfUploaded;
                }
                else {
                    $curBlockSize = $this->blockSize;
                }

                $chunkNum = ceil(($curBlockSize) / ($this->chunkSize));

                while($this->chunkNumOfUploaded < $chunkNum) {

                    //如果是是块的第一片上传，进行mkblk操作
                    if($this->chunkNumOfUploaded == 0) {

                        //mkblk
                        //如果当前文件剩余内容小于chunkSize,只会读取到EOF
                        $curChunk = fread($handle, $this->chunkSize);
                        $curChunkSize = strlen($curChunk);
                        $resp = $this->mkblk($curBlockSize, $curBlockNum, $token, $curChunk);
                        
                        //超时重试
                        if($resp->code == 28) {
                            $resp = $this->mkblkTimeout($curBlockSize, $curBlockNum, $token, $curChunk);
                        }

                        $this->mkblkHandler($resp, $curChunkSize);
                        echo 'mkblk:'.$curBlockNum."\n";
                    }
                    else {

                        //bput
                        $offsetOfChunk = $this->chunkNumOfUploaded * $this->chunkSize;

                        //释放chunk的内存空间
                        unset($curChunk);

                        //如果当前文件剩余内容小于chunkSize,只会读取到EOF
                        $curChunk = fread($handle, $this->chunkSize);
                        $curChunkSize = strlen($curChunk);
                        $resp = $this->bput($this->latestChunkCtx, $offsetOfChunk, $token, $curChunk);

                        //超时重试
                        if($resp->code == 28) {
                            $resp = $this->bputTimeout($this->latestChunkCtx, $offsetOfChunk, $token, $curChunk);
                        }

                        $this->bputHandler($resp, $curChunkSize);
                    }


                    //记录块最后一片的ctx
                    if($this->chunkNumOfUploaded == $chunkNum) {
                        array_push($this->ctxListForMkfile, $this->latestChunkCtx);
                    }

                    //记录当前上传的信息，方便断点续传
                    $resumeUploadInfo = array(
                        'time' => $this->time,
                        'filename' => $this->localFile,
                        'token' => $token,
                        'uuid' => $this->uuid,
                        'sizeOfFile' => $this->sizeOfFile,
                        'sizeOfUploaded' => $this->sizeOfUploaded,
                        'blockNumOfUploaded' => $this->blockNumOfUploaded,
                        'chunkNumOfUploaded' => $this->chunkNumOfUploaded,
                        'ctxListForMkfile' => $this->ctxListForMkfile,
                        'latestChunkCtx' => $this->latestChunkCtx
                    );

                    $info = json_encode($resumeUploadInfo)."\n";

                    fwrite($recordFile, $info);
                }

                $this->blockNumOfUploaded ++;
                $this->chunkNumOfUploaded = 0;


            }

        }

        if($this->sizeOfUploaded == $this->sizeOfFile) {

            $resp = $this->mkfile($token);

            //超时重试
            if($resp->code == 28) {
                $resp = $this->mkfileTimeout($token);
            } 
            
            $this->mkfileHandler($resp, $this->recordFile);
        }

        fclose($recordFile);
        fclose($handle);

        return $resp;

    }

    function mkblk($curBlockSize, $curBlockNum, $token, $nextChunk) {
        $url = Config::WCS_PUT_URL . '/mkblk/' . $curBlockSize . '/' . $curBlockNum;
        $mimeType = null;

        $httpHeaders = array(
            'Authorization: '.$token,
            'ContentType: application/octet-stream',
            'uploadBatch: '.$this->uuid
        );
        $fields = $nextChunk;

        $resp = \Wcs\http_post($url, $httpHeaders, $fields);

        return $resp;

    }

    function  bput($ctx, $nextChunkOffset, $token, $nextChunk) {
        $url = Config::WCS_PUT_URL . '/bput/' . $ctx . '/' . $nextChunkOffset;
        $mimeType = null;

        $httpHeaders = array(
            'Authorization: '.$token,
            'ContentType: application/octet-stream',
            'uploadBatch: '.$this->uuid
        );

        $fields = $nextChunk;

        $resp = \Wcs\http_post($url, $httpHeaders, $fields);

        return $resp;


    }

    function mkfile($token) {
        $url = Config::WCS_PUT_URL . '/mkfile/' . $this->sizeOfFile . '/';

        if($this->userParam !== null) {
            $url .= $this->userParam.'/';
        }

        if($this->userParam !== null && $this->encodedUserVars != null) {
            $url .= $this->encodedUserVars;
        }

        $httpHeaders = array(
            'Authorization: '.$token,
            'ContentType: text/plain;charset=UTF-8',
            'uploadBatch: '.$this->uuid,
            'key: '.$this->localFile,
            'mimeType '.$this->mimeType
        );


        $fields = implode($this->ctxListForMkfile, ",");
        $resp = \Wcs\http_post($url, $httpHeaders, $fields);

        return $resp;

    }

    function  mkblkHandler($resp, $curChunkSize) {
        if($resp->code == 200) {
            $this->chunkNumOfUploaded ++;
            $result = json_decode($resp->respBody, true);
            $this->latestChunkCtx = $result['ctx'];
            $this->sizeOfUploaded += $curChunkSize;

            return true;
        }
        else {
            if(filesize($this->recordFile) == 0) {
                $this->deleteRecord($this->recordFile);
            }

            print_r($resp);
            die("ERROR:mkblk失败！");
        }
    }

    function  bputHandler($resp, $curChunkSize) {
        if($resp->code == 200) {
            echo 'bput'.$this->chunkNumOfUploaded."\n";
            $this->chunkNumOfUploaded ++;
            $result = json_decode($resp->respBody, true);
            $this->latestChunkCtx = $result['ctx'];
            $this->sizeOfUploaded += $curChunkSize;

            return true;
        }
        else {
            print_r($resp);
            die("ERROR:bput失败");
        }
    }

    function  mkfileHandler($resp, $recordFile) {
        if($resp->code == 200) {
            echo $resp->respBody."\n";
            $this->deleteRecord($recordFile);
        }
        else {
            print_r($resp);
            die('ERROR:mkfile失败');
        }
    }

    function mkblkTimeout($curBlockSize, $curBlockNum, $token, $curChunk) {
        while($this->countForRetry){ 
            echo "超时重试{$this->countForRetry}\n";
            $resp = $this->mkblk($curBlockSize, $curBlockNum, $token, $curChunk);

            if($resp->code !== 28) {
                //重置countForRetry 
                $this->countForRetry = 3;
                return $resp; 
            }
            
            $this->countForRetry --;
        } 
        return $resp;
    }
    
    function bputTimeout($latestChunkCtx, $offsetOfChunk,  $token, $curChunk) {
        while($this->countForRetry){ 
            echo "超时重试{$this->countForRetry}\n";
            $resp = $this->bput($latestChunkCtx, $offsetOfChunk, $token, $curChunk);
            
            if($resp->code !== 28) {
                //重置countForRetry 
                $this->countForRetry = 3;
                return $resp; 
            }

            $this->countForRetry --;
        } 
        return $resp;
    }

     function mkfileTimeout($token) {
        while($this->countForRetry){ 
            echo "超时重试{$this->countForRetry}\n";
            $resp = $this->mkfile($token);
            
            if($resp->code !== 28) {
                //重置countForRetry 
                $this->countForRetry = 3;
                return $resp; 
            }

            $this->countForRetry --;
        } 
        return $resp;
    }

    function deleteRecord($recordFile) {
        if(is_file($recordFile)) {
            if(!unlink($recordFile)) {
                echo "记录文件{$recordFile}删除失败！";
            }
        }
    }

}

    


